// Copyright 2011 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package com.google.api.ads.adwords.lib.client;

import com.google.api.ads.common.lib.auth.ClientLoginCompatible;
import com.google.api.ads.common.lib.auth.OAuth2Compatible;
import com.google.api.ads.common.lib.auth.OAuthCompatible;
import com.google.api.ads.common.lib.client.AdsSession;
import com.google.api.ads.common.lib.client.Endpoint;
import com.google.api.ads.common.lib.conf.ConfigurationHelper;
import com.google.api.ads.common.lib.conf.ConfigurationLoadException;
import com.google.api.ads.common.lib.exception.ValidationException;
import com.google.api.client.auth.oauth.OAuthParameters;
import com.google.api.client.auth.oauth2.Credential;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;

import org.apache.commons.configuration.Configuration;

import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;

/**
 * A {@code AdWordsSession} represents a single session of AdWords use.
 *
 * <p>
 * Implementation is not thread-safe.
 * </p>
 *
 * @author Adam Rogal
 */
public class AdWordsSession
    implements
    AdsSession,
    OAuthCompatible,
    OAuth2Compatible,
    ClientLoginCompatible {

  /**
   * Enum representing the endpoint server, aka environment.
   * @author Kevin Winter
   * @deprecated
   */
  @Deprecated
  public static enum Environment implements Endpoint {
    PRODUCTION("https://adwords.google.com/");

    private final String endpoint;

    private Environment(String endpoint) {
      this.endpoint = endpoint;
    }

    /**
     * @return the endpoint
     */
    public String getEndpoint() {
      return endpoint;
    }
  }

  private String endpoint;
  private String clientLoginToken;
  private String userAgent;
  private String developerToken;
  private String clientCustomerId;
  private OAuthParameters oAuthParameters;
  private Boolean isPartialFailure;
  private Boolean isValidateOnly;
  private Boolean isReportMoneyInMicros = Boolean.FALSE;
  private Credential oAuth2Credential;

  public static final String DEFAULT_ENDPOINT = "https://adwords.google.com/";

  /**
   * Private constructor.
   */
  private AdWordsSession() {
  }

  /**
   * Copy Constructor.
   */
  private AdWordsSession(AdWordsSession copy) {
    this.clientCustomerId = copy.clientCustomerId;
    this.clientLoginToken = copy.clientLoginToken;
    this.developerToken = copy.developerToken;
    this.endpoint = copy.endpoint;
    this.isPartialFailure = copy.isPartialFailure;
    this.isReportMoneyInMicros = copy.isReportMoneyInMicros;
    this.isValidateOnly = copy.isValidateOnly;
    this.oAuth2Credential = copy.oAuth2Credential;
    this.oAuthParameters = copy.oAuthParameters;
    this.userAgent = copy.userAgent;
  }

  /**
   * Gets the client customer ID.
   */
  public String getClientCustomerId() {
    return clientCustomerId;
  }

  /**
   * Sets the client customer ID.
   */
  public void setClientCustomerId(String clientCustomerId) {
    this.clientCustomerId = clientCustomerId;
  }

  /**
   * Returns {@code true} if the session should only validate the request.
   */
  public Boolean isValidateOnly() {
    return isValidateOnly;
  }

  /**
   * Sets whether this session should only validate the request.
   */
  public void setValidateOnly(Boolean isValidateOnly) {
    this.isValidateOnly = isValidateOnly;
  }

  /**
   * Sets whether this session should allow partial failure.
   */
  public void setPartialFailure(Boolean isPartialFailure) {
    this.isPartialFailure = isPartialFailure;
  }

  /**
   * @return the userAgent
   */
  public String getUserAgent() {
    return userAgent;
  }

  /**
   * @return the developerToken
   */
  public String getDeveloperToken() {
    return developerToken;
  }

  /**
   * @return the isPartialFailure
   */
  public Boolean isPartialFailure() {
    return isPartialFailure;
  }

  /**
   * @return the oAuthParameters
   *
   * @deprecated It is encouraged that you switch to OAuth2 at your earliest
   *             convenience. Beginning in 1.9.0, the GoogleOAuth
   *             authorization flow will no longer be supported, making it
   *             substantially more difficult to create OAuth1.0a tokens.
   */
  @Deprecated
  public OAuthParameters getOAuthParameters() {
    return oAuthParameters;
  }

  /**
   * Sets the OAuth parameters. Any other authentication credentials on the
   * session will be removed.
   *
   * @deprecated It is encouraged that you switch to OAuth2 at your earliest
   *             convenience. Beginning in 1.9.0, the GoogleOAuth
   *             authorization flow will no longer be supported, making it
   *             substantially more difficult to create OAuth1.0a tokens.
   */
  @Deprecated
  public void setOAuthParameters(OAuthParameters oAuthParameters) {
    Preconditions.checkNotNull(oAuthParameters, "oAuthParameters cannot be null.");
    clearAuthentication();
    this.oAuthParameters = oAuthParameters;
  }

  /**
   * Gets the OAuth2 credentials.
   */
  public Credential getOAuth2Credential() {
    return oAuth2Credential;
  }

  /**
   * Sets the OAuth2 credential. Any other authentication credentials on the
   * session will be removed.
   */
  public void setOAuth2Credential(Credential oAuth2Credential) {
    Preconditions.checkNotNull(oAuth2Credential, "oAuth2Credential cannot be null.");
    clearAuthentication();
    this.oAuth2Credential = oAuth2Credential;
  }

  /**
   * Gets the client login token.
   */
  public String getClientLoginToken() {
    return clientLoginToken;
  }

  /**
   * Sets the ClientLogin Token. Any other authentication credentials on the
   * session will be removed.
   */
  public void setClientLoginToken(String clientLoginToken) {
    Preconditions.checkNotNull(clientLoginToken, "clientLoginToken cannot be null.");
    clearAuthentication();
    this.clientLoginToken = clientLoginToken;
  }

  /**
   * Clears all the authentication credentials from this session.
   */
  private void clearAuthentication() {
    oAuth2Credential = null;
    oAuthParameters = null;
    clientLoginToken = null;
  }

  /**
   * @return the endpoint
   */
  public String getEndpoint() {
    return endpoint;
  }

  /**
   * @param reportMoneyInMicros the reportMoneyInMicros to set
   */
  public void setReportMoneyInMicros(Boolean reportMoneyInMicros) {
    this.isReportMoneyInMicros = reportMoneyInMicros;
  }

  /**
   * @return the reportMoneyInMicros
   */
  public Boolean isReportMoneyInMicros() {
    return isReportMoneyInMicros;
  }

  /**
   * Determines if the session is for the supplied
   * {@link AdWordsSession.Environment}.
   *
   * @deprecated
   */
  @Deprecated
  public boolean isEnvironment(Endpoint environment) {
    return environment.getEndpoint().equals(this.endpoint);
  }

  /**
   * Builder for AdWordsSession.
   *
   * <p>
   * Implementation is not thread-safe.
   * </p>
   */
  public static class Builder implements
      com.google.api.ads.common.lib.utils.Builder<AdWordsSession> {

    private AdWordsSession adWordsSession;
    private String environmentString;
    @VisibleForTesting
    ConfigurationHelper configHelper;

    /**
     * Constructor.
     */
    public Builder() {
      this.adWordsSession = new AdWordsSession();
      this.configHelper = new ConfigurationHelper();
    }

    @VisibleForTesting
    Builder(ConfigurationHelper configHelper) {
      this.adWordsSession = new AdWordsSession();
      this.configHelper = configHelper;
    }

    public Builder fromFile() throws ConfigurationLoadException {
      return fromFile(Builder.DEFAULT_CONFIGURATION_FILENAME);
    }

    public Builder fromFile(String path) throws ConfigurationLoadException {
      return from(configHelper.fromFile(path));
    }

    public Builder fromFile(File path) throws ConfigurationLoadException {
      return from(configHelper.fromFile(path));
    }

    public Builder fromFile(URL path) throws ConfigurationLoadException {
      return from(configHelper.fromFile(path));
    }

    /**
     * Reads properties from the provided {@link Configuration} object.<br><br>
     * Known properties:
     * <ul>
     * <li>api.adwords.clientLoginToken</li>
     * <li>api.adwords.clientCustomerId</li>
     * <li>api.adwords.userAgent</li>
     * <li>api.adwords.developerToken</li>
     * <li>api.adwords.isPartialFailure</li>
     * <li>api.adwords.endpoint</li>
     * <li>api.adwords.reportMoneyInMicros</li>
     * <li>Deprecated: api.adwords.environment</li>
     * </ul>
     *
     * @param config
     * @return Builder populated from the Configuration
     */
    public Builder from(Configuration config) {
      adWordsSession.clientLoginToken = config.getString("api.adwords.clientLoginToken", null);
      adWordsSession.clientCustomerId = config.getString("api.adwords.clientCustomerId", null);
      adWordsSession.userAgent = config.getString("api.adwords.userAgent", null);
      adWordsSession.developerToken = config.getString("api.adwords.developerToken", null);
      adWordsSession.isPartialFailure = config.getBoolean("api.adwords.isPartialFailure", null);
      adWordsSession.endpoint = config.getString("api.adwords.endpoint", null);
      adWordsSession.isReportMoneyInMicros = config.getBoolean("api.adwords.reportMoneyInMicros",
          Boolean.FALSE);
      environmentString = config.getString("api.adwords.environment", null);

      return this;
    }

    /**
     * Includes hard-coded ClientLogin token that will be used instead of
     * fetching a new one.
     */
    public Builder withClientLoginToken(String clientLoginToken) {
      adWordsSession.setClientLoginToken(clientLoginToken);
      return this;
    }

    /**
     * Includes a developer token. Required.
     */
    public Builder withDeveloperToken(String developerToken) {
      adWordsSession.developerToken = developerToken;
      return this;
    }

    /**
     * Includes OAuth parameters to be used for OAuth authentication.
     *
     * @deprecated It is encouraged that you switch to OAuth2 at your earliest
     *             convenience. Beginning in 1.9.0, the GoogleOAuth
     *             authorization flow will no longer be supported, making it
     *             substantially more difficult to create OAuth1.0a tokens.
     */
    @Deprecated
    public Builder withOAuthParameters(OAuthParameters oAuthParameters) {
      adWordsSession.setOAuthParameters(oAuthParameters);
      return this;
    }

    /**
     * Includes OAuth2 credential to be used for OAuth2 authentication.
     */
    public Builder withOAuth2Credential(Credential oAuth2Credential) {
      adWordsSession.setOAuth2Credential(oAuth2Credential);
      return this;
    }

    /**
     * Includes user agent.
     */
    public Builder withUserAgent(String userAgent) {
      adWordsSession.userAgent = userAgent;
      return this;
    }

    /**
     * Includes the specified environment, e.g.
     * {@link Environment#PRODUCTION}
     *
     * The environment and endpoint in AdWords are currently synonymous. Setting
     * an environment will clear out any endpoint you have previously set.
     *
     * @deprecated Use {@link Builder#withEndpoint(String)}.
     */
    @Deprecated
    public Builder withEnvironment(Environment environment) {
      adWordsSession.endpoint = environment.getEndpoint();
      environmentString = null;
      return this;
    }

    /**
     * Override the endpoint server. Optional and defaults to
     * https://adwords.google.com.
     *
     * The environment and endpoint in AdWords are currently synonymous. Setting
     * an endpoint will clear out any environment you have previously set.
     */
    public Builder withEndpoint(String endpoint) {
      adWordsSession.endpoint = endpoint;
      environmentString = null;
      return this;
    }

    /**
     * Includes a clientCustomerId.
     */
    public Builder withClientCustomerId(String clientCustomerId) {
      adWordsSession.clientCustomerId = clientCustomerId;
      return this;
    }

    /**
     * Enables partial failure. Default is disabled.
     */
    public Builder enablePartialFailure() {
      adWordsSession.isPartialFailure = true;
      return this;
    }

    /**
     * Enable downloading of money values in reports in micros.
     */
    public Builder enableReportMoneyInMicros() {
      adWordsSession.isReportMoneyInMicros = true;
      return this;
    }

    /**
     * Builds the {@code AdWordsSession}.
     *
     * @return the built {@code AdWordsSession}
     * @throws ValidationException if the {@code AdWordsSession} did not validate
     */
    public AdWordsSession build() throws ValidationException {
      defaultOptionals();
      validate(adWordsSession);
      return new AdWordsSession(adWordsSession);
    }

    /**
     * Fills in defaults if {@code null}.
     */
    private void defaultOptionals() {
      if (adWordsSession.endpoint == null) {
        adWordsSession.endpoint = DEFAULT_ENDPOINT;
      }
    }

    /**
     * Validates the AdWords session.
     */
    private void validate(AdWordsSession adWordsSession) throws ValidationException {
      if (environmentString != null) {
        try {
          withEnvironment(Environment.valueOf(environmentString.toUpperCase()));
        } catch (IllegalArgumentException e) {
          throw new ValidationException(String.format("Environment [%s] not recognized.",
              environmentString), "api.adwords.environment", e);
        }
      }

      // Check for valid OAuth or ClientLogin information.
      if (adWordsSession.getClientLoginToken() != null
          && (adWordsSession.getOAuthParameters() != null
              || adWordsSession.getOAuth2Credential() != null)) {
        throw new ValidationException("Only one of ClientLogin authentication (clientLoginToken), "
            + " OAuth or OAuth2 authentication cannot be used at the same time.", "");
      }

      // Check for valid OAuth/2 information.
      if (adWordsSession.getClientLoginToken() == null
          && (adWordsSession.getOAuthParameters() != null
              && adWordsSession.getOAuth2Credential() != null)) {
        throw new ValidationException("Both OAuth and OAuth2 cannot be used at the same time.",
            "");
      }

      // Check for at least one authentication mechanism.
      if (adWordsSession.getClientLoginToken() == null
          && adWordsSession.getOAuthParameters() == null
          && adWordsSession.getOAuth2Credential() == null) {
        throw new ValidationException(
            "One of ClientLogin authentication, OAuth, or OAuth2 authentication must be used.", "");
      }

      // Check that the developer token is set.
      if (adWordsSession.getDeveloperToken() == null) {
        throw new ValidationException("A developer token must be set.", "developerToken");
      }

      // Make sure they specify an environment.
      try {
        new URL(adWordsSession.getEndpoint());
      } catch (MalformedURLException e) {
        throw new ValidationException(String.format("Endpoint [%s] not recognized as a valid URL.",
            adWordsSession.getEndpoint()), "endpoint", e);
      }
    }
  }
}
